Skip to content

fix: remove the delay in enabling of links after dragging#3697

Open
Arukuen wants to merge 1 commit intodevelopfrom
fix/3694-horizontal-scroller-links
Open

fix: remove the delay in enabling of links after dragging#3697
Arukuen wants to merge 1 commit intodevelopfrom
fix/3694-horizontal-scroller-links

Conversation

@Arukuen
Copy link
Copy Markdown
Contributor

@Arukuen Arukuen commented May 7, 2026

fixes #3694

Disabling of links when dragging and re-enabling them are introduced here: #2666. However, the re-enabling functionality is added to the snapping delay, making the links still unnecessarily disabled at that point.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed an issue in the horizontal scroller component where event listeners were being duplicated during drag interactions, improving performance and reliability.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR fixes first-click consumption on Horizontal Scroller buttons by correcting event listener lifecycle management. The disabled-link click listeners are now removed at the end of drag interactions (mouseUpHandler) instead of during drag processing, preventing stale listeners from accumulating and blocking clicks.

Changes

Horizontal Scroller Event Listener Cleanup

Layer / File(s) Summary
Event Listener Cleanup Timing
src/block/horizontal-scroller/frontend-horizontal-scroller.js
Moves children.forEach(... removeEventListener ...) from dragTimeout callback to mouseUpHandler to ensure disabled-link listeners are removed after smooth snap scroll completes, preventing listener accumulation that was consuming first clicks.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A scroller's heart beats clean and free,
No duplicate listeners cluttering the spree,
When dragging ends, the links come alive,
First clicks now work—the buttons thrive!
Much better than before, don't you agree?

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: removing a delay in the link re-enabling logic after dragging, which directly addresses the issue described in the pull request objectives.
Linked Issues check ✅ Passed The PR addresses the core objective from issue #3694 by decoupling link re-enabling from the snapping delay, ensuring links function on first click after load or focus changes.
Out of Scope Changes check ✅ Passed All changes are scoped to the horizontal-scroller mouse drag handler lifecycle, directly related to fixing link-disabling logic without introducing unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3694-horizontal-scroller-links

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

🤖 Pull request artifacts

file commit
pr3697-stackable-3697-merge.zip dcfe5dc

github-actions Bot added a commit that referenced this pull request May 7, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/block/horizontal-scroller/frontend-horizontal-scroller.js (2)

9-9: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

dragTimeout is shared across all scroller elements

dragTimeout is declared once in init() scope and shared by every element's mouseDownHandler. On a page with multiple horizontal scrollers, scroller B's mousedown will clearTimeout(dragTimeout) mid-flight for scroller A, leaving scroller A permanently stuck with the stk--snapping-deactivated class.

🐛 Proposed fix — scope `dragTimeout` per element
         els.forEach( el => {
+            let dragTimeout = null
             if ( el._StackableHasInitHorizontalScroller ) {
-        let dragTimeout = null
         let initialScrollLeft = 0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/block/horizontal-scroller/frontend-horizontal-scroller.js` at line 9, The
shared dragTimeout declared in init() causes cross-element interference; make
the timeout instance-scoped per scroller element by removing the single outer
let dragTimeout and instead attach a per-element timeout (e.g., el._dragTimeout
or a local let inside the per-element setup) and update all references in
mouseDownHandler, mouseMoveHandler and mouseUpHandler to use that per-element
property; ensure clearTimeout and setting to null happen on the same element
(and on cleanup) so one scroller’s mousedown won’t cancel another’s snapping
state (stk--snapping-deactivated).

53-79: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stale click-blockers persist when the browser loses focus mid-drag (alt-tab / tab switch)

The fix correctly moves link re-enablement from inside the 500 ms timeout into mouseUpHandler. However, mouseUpHandler only runs when mouseup fires on document.body. Changing desktops or tabs while the mouse is held down results in no mouseup event on document in either Firefox or Chrome on Windows. This means:

  1. User starts dragging → mouseMoveHandler attaches onClickHandler to every child.
  2. User alt-tabs while holding the button → mouseup never fires → mouseUpHandler never runs → click listeners remain on children.
  3. User returns to the tab and clicks a link → first click is consumed (re-creates the original bug).

This matches the issue's stated symptom: "The behavior recurs when a browser tab loses and regains focus."

The well-known mitigation is to treat window.blur as a drag-end signal and run the same cleanup:

🛡️ Proposed fix — add a `window` blur handler
         const mouseUpHandler = function() {
             document.body.removeEventListener( 'mousemove', mouseMoveHandler )
             document.body.removeEventListener( 'mouseup', mouseUpHandler )
+            window.removeEventListener( 'blur', mouseUpHandler )

             el.style.cursor = ''
             // ... rest unchanged
         }

         const mouseDownHandler = function( e ) {
             // ...
             document.body.addEventListener( 'mousemove', mouseMoveHandler )
             document.body.addEventListener( 'mouseup', mouseUpHandler )
+            window.addEventListener( 'blur', mouseUpHandler )
         }

One effective mitigation is to add an additional blur event handler on window; the blur event fires when the window loses focus, and ending any drags in progress when this event fires produces a safe approximation of the true mouse state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/block/horizontal-scroller/frontend-horizontal-scroller.js` around lines
53 - 79, The drag cleanup in mouseUpHandler never runs if the tab/window loses
focus mid-drag, leaving click-blockers attached; add a window 'blur' listener
when starting a drag (where mouseMoveHandler is attached) that invokes the same
cleanup as mouseUpHandler (or calls mouseUpHandler) so children have
onClickHandler removed, dragTimeout cleared/handled, and class toggles restored;
also ensure the blur listener is removed inside mouseUpHandler after performing
the cleanup to avoid leaks.
🧹 Nitpick comments (1)
src/block/horizontal-scroller/frontend-horizontal-scroller.js (1)

44-50: 💤 Low value

Link-disabling listeners are attached on every mousemove event

children.forEach(child => child.addEventListener('click', onClickHandler, true)) runs once per mousemove, i.e., once per pixel of movement. While addEventListener deduplicates identical listeners (same reference + type + capture), the forEach loop still executes on every event. These listeners only need to be attached once per drag — move them to mouseDownHandler.

♻️ Proposed refactor
         const mouseMoveHandler = function( e ) {
             const dx = e.clientX - initialClientX
             el.scrollTo( { left: initialScrollLeft - dx } )
             e.preventDefault()
-            children.forEach( child => {
-                child.addEventListener( 'click', onClickHandler, true )
-            } )
         }

         const mouseDownHandler = function( e ) {
             el.style.cursor = 'grabbing'
             clearTimeout( dragTimeout )
             el.classList.add( 'stk--snapping-deactivated' )
             initialScrollLeft = el.scrollLeft
             initialClientX = e.clientX
             document.body.addEventListener( 'mousemove', mouseMoveHandler )
             document.body.addEventListener( 'mouseup', mouseUpHandler )
+            window.addEventListener( 'blur', mouseUpHandler )
+            children.forEach( child => {
+                child.addEventListener( 'click', onClickHandler, true )
+            } )
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/block/horizontal-scroller/frontend-horizontal-scroller.js` around lines
44 - 50, The click-disabling listeners are being added inside the mousemove flow
causing redundant per-pixel iterations; move the child.addEventListener('click',
onClickHandler, true) loop out of the mouseMoveHandler and into mouseDownHandler
so listeners are attached once per drag start (use the same onClickHandler
reference), and ensure you remove them in mouseUpHandler via
child.removeEventListener('click', onClickHandler, true) to restore normal link
behavior after the drag.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/block/horizontal-scroller/frontend-horizontal-scroller.js`:
- Line 9: The shared dragTimeout declared in init() causes cross-element
interference; make the timeout instance-scoped per scroller element by removing
the single outer let dragTimeout and instead attach a per-element timeout (e.g.,
el._dragTimeout or a local let inside the per-element setup) and update all
references in mouseDownHandler, mouseMoveHandler and mouseUpHandler to use that
per-element property; ensure clearTimeout and setting to null happen on the same
element (and on cleanup) so one scroller’s mousedown won’t cancel another’s
snapping state (stk--snapping-deactivated).
- Around line 53-79: The drag cleanup in mouseUpHandler never runs if the
tab/window loses focus mid-drag, leaving click-blockers attached; add a window
'blur' listener when starting a drag (where mouseMoveHandler is attached) that
invokes the same cleanup as mouseUpHandler (or calls mouseUpHandler) so children
have onClickHandler removed, dragTimeout cleared/handled, and class toggles
restored; also ensure the blur listener is removed inside mouseUpHandler after
performing the cleanup to avoid leaks.

---

Nitpick comments:
In `@src/block/horizontal-scroller/frontend-horizontal-scroller.js`:
- Around line 44-50: The click-disabling listeners are being added inside the
mousemove flow causing redundant per-pixel iterations; move the
child.addEventListener('click', onClickHandler, true) loop out of the
mouseMoveHandler and into mouseDownHandler so listeners are attached once per
drag start (use the same onClickHandler reference), and ensure you remove them
in mouseUpHandler via child.removeEventListener('click', onClickHandler, true)
to restore normal link behavior after the drag.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab963f70-c704-4338-aa6e-799bbade8cfe

📥 Commits

Reviewing files that changed from the base of the PR and between 2f30005 and dcfe5dc.

📒 Files selected for processing (1)
  • src/block/horizontal-scroller/frontend-horizontal-scroller.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Horizontal Scroller: first click on buttons consumed by drag handler initialization

1 participant